//
// Created by hamlet on 2022/1/18.
//

#include "snakegame.h"
#include <memory>
#include <QTimerEvent>
#include <QPainter>
#include <QRandomGenerator>
#include "player.h"
#include "room.h"
#include "messageutil.h"

int WorldWidget::TIME{0};

Snake::Snake(const PlayerInfo *player, QSize world_size, QPoint start_p, QPoint end_p)
        : p_player(player), m_worldSize(world_size), m_tail(end_p) {
    m_body.push_front(start_p);
}

Snake::~Snake() {
    for (auto it: m_effectiveFruits) delete it;
}

bool Snake::advance() {
    if (m_dieTime != -1) {
        if (WorldWidget::TIME - m_dieTime > DYING_TIME)
            return false;
        return true;
    }
    setDirection(p_nextDirection);
    p_nextDirection = Direction::None;
    QPoint &head = m_body.front();
    head += m_velocity_head;
    if (head.x() < 0) {
        makeAnchorPoint();
        m_body.front().setX(m_worldSize.width() - 1);
        makeAnchorPoint();
    } else if (head.x() >= m_worldSize.width()) {
        makeAnchorPoint();
        m_body.front().setX(0);
        makeAnchorPoint();
    } else if (head.y() < 0) {
        makeAnchorPoint();
        m_body.front().setY(m_worldSize.height() - 1);
        makeAnchorPoint();
    } else if (head.y() >= m_worldSize.height()) {
        makeAnchorPoint();
        m_body.front().setY(0);
        makeAnchorPoint();
    }
    // 以下涉及果子的过程只是暂时操作
    for (int i = (int) m_effectiveFruits.size() - 1; i >= 0; --i) {
        auto fruit = m_effectiveFruits[i];
        if (WorldWidget::TIME - fruit->p_ateTime > 5) {
            delete fruit;
            m_effectiveFruits.erase(m_effectiveFruits.cbegin() + i);
        }
    }
    bool isGrowing = !m_effectiveFruits.empty();
    if (isGrowing)
        return true;

    m_tail += m_velocity_tail;
    while (m_tail == m_body.back()) {
        m_body.pop_back();
        if (m_tail.x() < 0 || m_tail.x() >= m_worldSize.width() ||
            m_tail.y() < 0 || m_tail.y() >= m_worldSize.height()) {
            m_tail = m_body.back();
            m_body.pop_back();
        } else {
            const QPoint &tail = m_body.back();
            if (m_tail.y() == tail.y()) {
                if (m_tail.x() < tail.x())
                    m_velocity_tail = F;
                else
                    m_velocity_tail = B;
            } else {
                if (m_tail.y() < tail.y())
                    m_velocity_tail = N;
                else
                    m_velocity_tail = P;
            }
        }
    }
    return true;
}

bool Snake::isInside(const QPoint &p) const {
    QPoint p1 = m_tail, p2 = *(m_body.rbegin());
    for (auto it = m_body.rbegin(); it != m_body.rend(); p1 = p2, p2 = *(++it)) {
        if (p1.x() < 0 || p1.x() >= m_worldSize.width() || p.y() < 0 || p.y() >= m_worldSize.height())
            continue;
        if (p1 == p2) {
            if (p == p1)
                return true;
        } else {
            if (p1.x() == p2.x()) {
                if (p.x() == p1.x() &&
                    ((p.y() >= p1.y() && p.y() <= p2.y()) || (p.y() >= p2.y() && p.y() <= p1.y())))
                    return true;
            } else {
                if (p.y() == p1.y() &&
                    ((p.x() >= p1.x() && p.x() <= p2.x()) || (p.x() >= p2.x() && p.x() <= p1.x())))
                    return true;
            }
        }
    }
    return false;
}

bool Snake::isSelfInside() const {
    QPoint head = m_body.front(), p1 = m_tail, p2 = *(m_body.rbegin());
    for (auto it = m_body.rbegin(); p2 != head; p1 = p2, p2 = *(++it)) {
        if (p1.x() < 0 || p1.x() >= m_worldSize.width() || p1.y() < 0 || p1.y() >= m_worldSize.height())
            continue;
        if (p1 == p2) {
            if (head == p1)
                return true;
        } else {
            if (p1.x() == p2.x()) {
                if (head.x() == p1.x() &&
                    ((head.y() >= p1.y() && head.y() <= p2.y()) || (head.y() >= p2.y() && head.y() <= p1.y())))
                    return true;
            } else {
                if (head.y() == p1.y() &&
                    ((head.x() >= p1.x() && head.x() <= p2.x()) || (head.x() >= p2.x() && head.x() <= p1.x())))
                    return true;
            }
        }
    }
    return false;
}

void Snake::die() {
    m_dieTime = WorldWidget::TIME;
}

void Snake::addEffectiveFruit(NewFruitMessage *fruit) {
    m_effectiveFruits.push_back(new Fruit{fruit});
    qDebug() << "ate a fruit" << fruit->p_timeStamp;
}

void Snake::setDirection(Snake::Direction dir) {
    switch (dir) {
        case Direction::P:
            m_velocity_head = P;
            break;
        case Direction::N:
            m_velocity_head = N;
            break;
        case Direction::B:
            m_velocity_head = B;
            break;
        case Direction::F:
            m_velocity_head = F;
            break;
        case Direction::None:
            return;
    }
    makeAnchorPoint();
}

bool Snake::checkDirection(Snake::Direction dir) {
    if (dir == Direction::P || dir == Direction::N) {
        return m_velocity_head.x();
    } else {
        return m_velocity_head.y();
    }
}

WorldWidget::~WorldWidget() {
    setRoom(nullptr);
    for (auto it: m_fruitList) delete it;
    for (auto it: m_snakes) delete it.second;
}

void WorldWidget::setRoom(const RoomInfo *room) {
    for (auto it: m_snakes)
        delete it.second;
    m_snakes.clear();
    if (!room) {
        stopGame();
        return;
    }
    m_room = room;
    switch (m_room->getSizeType()) {
        case RoomInfo::SizeType::Big:
            m_worldSize = RoomInfo::SIZE_BIG;
            break;
        case RoomInfo::SizeType::Medium:
            m_worldSize = RoomInfo::SIZE_MEDIUM;
            break;
        case RoomInfo::SizeType::Small:
            m_worldSize = RoomInfo::SIZE_SMALL;
            break;
        case RoomInfo::SizeType::Custom:
            m_worldSize = m_room->getSize();
            break;
    }
    resizeEvent(nullptr);
}

void WorldWidget::addSnake(const PlayerInfo *player) {
    if (!m_snakes.contains(player->getID())) {
        int y = 20 * (int) m_snakes.size();
        m_snakes.insert({player->getID(),
                         new Snake{player, m_worldSize, {20, y}, {10, y}}}
        );
    }
}

void WorldWidget::setSnakeDirection(SnakeDirectionChangeMessage *message) {
    if (m_snakes.contains(message->p_playerID)) {
        auto snake = m_snakes[message->p_playerID];
        snake->p_nextDirection = message->p_direction;
        if (message->p_timeStamp < m_timeStamp) {
            qDebug() << "The network latency is too high, and the game cannot be synchronized.";
        }
    }
}

void WorldWidget::client_newFruit(NewFruitMessage *message) {
    m_fruitList.append(message);
}

void WorldWidget::startGame(int time_stamp) {
    if (!m_timerID) {
        m_timerID = startTimer(TIME_STEP, Qt::PreciseTimer);
        m_timeStamp = time_stamp;
        if (p_config_isServer) {
            m_timerID_fruit = startTimer(TIME_STEP_FRUIT, Qt::PreciseTimer);
        }
        resizeEvent(nullptr);
    }
}

void WorldWidget::stopGame() {
    killTimer(m_timerID);
    if (m_timerID_fruit) killTimer(m_timerID_fruit);
}

inline int square(int x) {
    return x * x;
}

void WorldWidget::timerEvent(QTimerEvent *event) {
    TIME = QTime::currentTime().msecsSinceStartOfDay() >> STEP;
    if (TIME > m_timeStamp) {
        int timer_id = event->timerId();
        if (timer_id == m_timerID) {
            m_timeStamp = TIME;
            for (auto it = m_snakes.begin(); it != m_snakes.end();) {
                auto it_tmp = it;
                ++it;
                if (!it_tmp->second->advance())
                    snakeCrashed(it_tmp->second->p_player->getID());
            }
            std::vector<Snake *> died_snakes;
            for (auto it = m_snakes.begin(); it != m_snakes.end();) {
                auto snake1 = it->second;
                if (!snake1->isLiving()) {
                    ++it;
                    continue;
                }
                if (snake1->isSelfInside())
                    died_snakes.push_back(snake1);
                for (auto it2 = (++it); it2 != m_snakes.end(); ++it2) {
                    auto snake2 = it->second;
                    if (snake2->isInside(snake1->getHead()))
                        died_snakes.push_back(snake1);
                    if (snake2->isLiving() && snake1->isInside(snake2->getHead()))
                        died_snakes.push_back(snake2);
                }
                for (auto it_fr = m_fruitList.cbegin(); it_fr != m_fruitList.cend(); ++it_fr) {
                    auto fruit = *it_fr;
                    if (TIME < fruit->p_timeStamp) continue;
                    if (snake1->getHead() == fruit->p_fruitPos) {
                        snake1->addEffectiveFruit(fruit);
                        m_fruitList.erase(it_fr);
                        break;
                    }
                }
            }
            for (auto snake: died_snakes)
                snake->die();
        } else if (timer_id == m_timerID_fruit) {  // server
            QPoint fruit_pos;
            bool non_qualified;
            do {
                fruit_pos = {QRandomGenerator::global()->bounded(0, m_worldSize.width()),
                             QRandomGenerator::global()->bounded(0, m_worldSize.height())};
                non_qualified = false;
                for (auto it: m_snakes) {
                    auto head = it.second->getHead();
                    if (it.second->isInside(fruit_pos) ||
                        square(fruit_pos.x() - head.x()) + square(fruit_pos.y() - head.y()) <= 4) {
                        non_qualified = true;
                        break;
                    }
                }
                if (non_qualified) continue;
                for (auto fruit: m_fruitList) {
                    if (fruit_pos == fruit->p_fruitPos) {
                        non_qualified = true;
                        break;
                    }
                }
            } while (non_qualified);
            auto fruit_type = static_cast<RoomInfo::FruitType>(QRandomGenerator::global()->bounded(0, 11));
            NewFruitMessage message{fruit_pos, fruit_type, TIME + 2};
            emit sign_server_newFruit(&message);
        }
        repaint();
    }
    event->accept();
}

void WorldWidget::resizeEvent(QResizeEvent *) {
    auto geo = this->geometry();
    geo.setSize(geo.size() - QSize{MARGIN + MARGIN, MARGIN + MARGIN});
    qreal w_sep = static_cast<qreal>(geo.width()) / m_worldSize.width();
    qreal h_sep = static_cast<qreal>(geo.height()) / m_worldSize.height();
    m_basePoint = {MARGIN, MARGIN};
    if (w_sep >= h_sep) {
        m_canvasRect.setSize({geo.height() * m_worldSize.width() / m_worldSize.height(), geo.height()});
        m_basePoint += {(geo.width() - m_canvasRect.width()) / 2, 0};
        m_blockSize = h_sep;
    } else {
        m_canvasRect.setSize({geo.width(), geo.width() * m_worldSize.height() / m_worldSize.width()});
        m_basePoint += {0, (geo.height() - m_canvasRect.height()) / 2};
        m_blockSize = w_sep;
    }
    m_canvasRect.setTopLeft(m_basePoint);
}

void WorldWidget::paintEvent(QPaintEvent *event) {
    if (isGaming()) {
        QPainter painter(this);
        painter.setRenderHint(QPainter::Antialiasing, true);
        painter.setPen(QPen{Qt::blue, MARGIN, Qt::SolidLine});
        painter.setBrush(QBrush{Qt::white});
        painter.drawRoundedRect(m_canvasRect, 5.0, 5.0, Qt::RelativeSize);
        painter.setPen(QPen{Qt::black, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin});
        for (auto it: m_snakes) {
            it.second->paint(&painter, m_basePoint, m_blockSize);
        }
        for (auto fruit: m_fruitList) {
            if (TIME < fruit->p_timeStamp) continue;
            auto p = fruit->p_fruitPos;
            painter.setBrush(QBrush{Qt::red});
            painter.drawRoundedRect((int) (p.x() * m_blockSize) + m_basePoint.x(),
                                    (int) (p.y() * m_blockSize) + m_basePoint.y(),
                                    (int) m_blockSize, (int) m_blockSize, 5.0, 5.0);
        }
    }
}

void Snake::paint(QPainter *painter, const QPoint &base_point, qreal block_size) {
    if (m_dieTime != -1 && (WorldWidget::TIME - m_dieTime) % 2)
        return;
    painter->save();
    painter->setBrush(QBrush{p_player->getSnakeColor()});
    QPoint tmp = m_tail;
    for (auto it = m_body.rbegin(); it != m_body.rend(); ++it) {
        QPoint p = *it, start_p;
        bool teleport = true;
        if (p.x() < 0) p.setX(0);
        else if (p.x() >= m_worldSize.width()) p.setX(m_worldSize.width() - 1);
        else if (p.y() < 0) p.setY(0);
        else if (p.y() >= m_worldSize.height()) p.setY(m_worldSize.height() - 1);
        else teleport = false;
        int w = 1, h = 1;
        if (tmp.y() == p.y()) {
            if (tmp.x() < p.x()) {
                start_p = tmp;
                w += p.x() - tmp.x();
            } else {
                start_p = p;
                w += tmp.x() - p.x();
            }
        } else {
            if (tmp.y() < p.y()) {
                start_p = tmp;
                h += p.y() - tmp.y();
            } else {
                start_p = p;
                h += tmp.y() - p.y();
            }
        }
        painter->drawRoundedRect((int) (start_p.x() * block_size) + base_point.x(),
                                 (int) (start_p.y() * block_size) + base_point.y(),
                                 (int) (w * block_size), (int) (h * block_size),
                                 5.0, 5.0);
        tmp = p;
        if (teleport) {
            ++it;
            tmp = *it;
        }
    }
    painter->setBrush(QBrush{p_player->getSnakeColor().lighter()});
    painter->drawRoundedRect((int) (m_body.head().x() * block_size) + base_point.x(),
                             (int) (m_body.head().y() * block_size) + base_point.y(),
                             (int) block_size, (int) block_size,
                             5.0, 5.0);
    painter->restore();
}

bool WorldWidget::checkDirection(qint64 id, Snake::Direction dir) {
    return m_snakes[id]->checkDirection(dir);
}

void WorldWidget::snakeOut(qint64 id) {
    delete m_snakes[id];
    m_snakes.erase(id);
}

void WorldWidget::snakeCrashed(qint64 id) {
    snakeOut(id);
    emit sign_snakeCrashed(id);
}

Fruit::Fruit(NewFruitMessage *fruit_message) :
        p_ateTime(WorldWidget::TIME), p_fruitMessage(fruit_message) {
}

Fruit::~Fruit() {
    delete p_fruitMessage;
}
